package utsbia;

import robocode.*;
import robocode.util.Utils;
import java.awt.geom.*; // for Point2D's
import java.util.ArrayList; // for collection of waves
import java.util.HashMap;
import java.util.Random;
import java.awt.Color;
import java.awt.Graphics2D;
import java.io.IOException;

/**
 * A combination of the movement from BasicSurfer and the gun from
 * GFTargetingBot. See: http://robowiki.net?BasicSurfer
 * http://robowiki.net?GFTargetingBot
 */

public class SurferRobot extends TeamRobot {
	public static int BINS = 47;
	public static double _surfStats[] = new double[BINS]; // we'll use 47 bins
	public Point2D.Double _myLocation; // our bot's location
	public Point2D.Double _enemyLocation; // enemy bot's location
	public Point2D.Double _partnerTarget; // team member's target

	public ArrayList<EnemyWave> _enemyWaves;
	public ArrayList<Integer> _surfDirections;
	public ArrayList<Double> _surfAbsBearings;

	private static double lateralDirection;
	private static double lastEnemyVelocity;

	// We must keep track of the enemy's energy level to detect EnergyDrop,
	// indicating a bullet is fired
	public static double _oppEnergy = 100.0;

	// Rectangle representing the battle field used for wall smoothing
	public static Rectangle2D.Double _fieldRect = new java.awt.geom.Rectangle2D.Double(
			18, 18, 764, 564);
	public static double WALL_STICK = 160;

	protected long robotId;
	protected HashMap<Long, Point2D.Double> teamPositions;
	private boolean isTeamMates = true;

	public SurferRobot() {
		Random random = new Random();
		robotId = random.nextLong();

		teamPositions = new HashMap<Long, Point2D.Double>();
	}

	public void run() {
		setColors(Color.BLUE, Color.BLACK, Color.YELLOW);
		lateralDirection = 1;
		lastEnemyVelocity = 0;

		_enemyWaves = new ArrayList<EnemyWave>();
		_surfDirections = new ArrayList<Integer>();
		_surfAbsBearings = new ArrayList<Double>();

		setAdjustGunForRobotTurn(true);
		setAdjustRadarForGunTurn(true);

		do {
			turnRadarRightRadians(Double.POSITIVE_INFINITY);
		} while (true);
	}

	public void onStatus(StatusEvent e) {
		try {
			// Broadcast this robots position to the team
			broadcastMessage(new FriendlyPositionMessage(new Point2D.Double(
					getX(), getY()), robotId));
		} catch (IOException e1) {
			e1.printStackTrace();
		}
	}

	public void onMessageReceived(MessageEvent e) {
		if (e.getMessage() instanceof EnemyPositionMessage) {
			_partnerTarget = ((EnemyPositionMessage) e.getMessage())
					.getPosition();
		} else if (e.getMessage() instanceof EnemyAimOrderMessage) {
			EnemyAimOrderMessage message = (EnemyAimOrderMessage) e
					.getMessage();
			double dx = message.position.x - getX();
			double dy = message.position.y - getY();

			double absBearing = Math.atan2(dx, dy);
			double bearingRadians = absBearing - getHeadingRadians();

			double distance = Math.sqrt(Math
					.pow(message.position.x - getX(), 2)
					+ Math.pow(message.position.y - getY(), 2));

			shootEnemy(bearingRadians, distance, message.speed, message.angle);
		} else if (e.getMessage() instanceof FriendlyBulletMessage) {
			FriendlyBulletMessage message = (FriendlyBulletMessage) e
					.getMessage();
			logBullet(message);
		} else if (e.getMessage() instanceof EnemyBulletMessage) {
			EnemyBulletMessage message = (EnemyBulletMessage) e.getMessage();
			logBullet(message);
		} else if (e.getMessage() instanceof FriendlyPositionMessage) {
			FriendlyPositionMessage message = (FriendlyPositionMessage) e
					.getMessage();
			teamPositions.put(message.getRobotId(), message.getPosition());
		}
	}

	private void logBullet(FriendlyBulletMessage message) {
		double lateralVelocity = getVelocity() * Math.sin(message.getAngle());

		_surfDirections.add(0, new Integer((lateralVelocity >= 0) ? 1 : -1));
		_surfAbsBearings.add(0, new Double(message.getAngle() + Math.PI));

		logWave((long) message.getStartTime(), message.getSpeed(),
				message.getSpeed(), message.getOrigin());
	}

	private void logBullet(EnemyBulletMessage message) {
		double dx = message.getOrigin().x - getX();
		double dy = message.getOrigin().y - getY();

		double absBearing = Math.atan2(dx, dy);
		double lateralVelocity = getVelocity()
				* Math.sin(absBearing - getHeadingRadians());

		_surfDirections.add(0, new Integer((lateralVelocity >= 0) ? 1 : -1));
		_surfAbsBearings.add(0,
				new Double(Utils.normalRelativeAngle(absBearing + Math.PI)));

		logWave((long) message.getStartTime(), message.getSpeed(),
				message.getSpeed(), message.getOrigin());
	}

	private void logWave(long startTime, double bulletVel, double distTrav,
			Point2D.Double origin) {
		EnemyWave ew = new EnemyWave();
		ew.fireTime = startTime;
		ew.bulletVelocity = bulletVel;
		ew.distanceTraveled = distTrav;
		ew.fireLocation = origin;
		ew.direction = _surfDirections.get(2).intValue();
		ew.directAngle = _surfAbsBearings.get(2).doubleValue();

		_enemyWaves.add(ew);
	}

	public void onScannedRobot(ScannedRobotEvent e) {
		if (isTeammate(e.getName())) {
			// Don't target team members
			return;
		}

		_myLocation = new Point2D.Double(getX(), getY());

		double lateralVelocity = getVelocity()
				* Math.sin(e.getBearingRadians());
		double absBearing = e.getBearingRadians() + getHeadingRadians();

		setTurnRadarRightRadians(Utils.normalRelativeAngle(absBearing
				- getRadarHeadingRadians()) * 2);

		_surfDirections.add(0, new Integer((lateralVelocity >= 0) ? 1 : -1));
		_surfAbsBearings.add(0, new Double(absBearing + Math.PI));

		double bulletPower = _oppEnergy - e.getEnergy();
		if (bulletPower < 3.01 && bulletPower > 0.09
				&& _surfDirections.size() > 2) {
			logWave(getTime() - 1, bulletVelocity(bulletPower),
					bulletVelocity(bulletPower),
					(Point2D.Double) _enemyLocation.clone());

			// Notify team of enemy bullet fired
			EnemyBulletMessage message = new EnemyBulletMessage(project(
					_myLocation, absBearing, e.getDistance()),
					(double) getTime() - 1, bulletVelocity(bulletPower));
			try {
				broadcastMessage(message);
			} catch (IOException e1) {
				e1.printStackTrace();
			}
		}

		_oppEnergy = e.getEnergy();

		_enemyLocation = project(_myLocation, absBearing, e.getDistance());

		updateWaves();
		doSurfing();

		if (_partnerTarget != null) {
			// Determine if we are targeting the same opponents as a team member
			double xDiff = _enemyLocation.x - _partnerTarget.x;
			double yDiff = _enemyLocation.y - _partnerTarget.y;

			if (Math.abs(xDiff) <= 30 && Math.abs(yDiff) <= 30) {
				// Targeting the same opponent, scan for another
				setTurnRadarRight(360);
				return;
			}
		}

		if (!isTeamMates) {
			shootEnemy(e.getBearingRadians(), e.getDistance(), e.getVelocity(),
					e.getHeadingRadians());
		}
	}

	private void shootEnemy(double bearingRadians, double distance,
			double velocity, double headingRadians) {
		double enemyAbsoluteBearing = getHeadingRadians() + bearingRadians;
		double enemyDistance = distance;
		double enemyVelocity = velocity;
		if (enemyVelocity != 0) {
			lateralDirection = GFTUtils.sign(enemyVelocity
					* Math.sin(headingRadians - enemyAbsoluteBearing));
		}
		GFTWave wave = new GFTWave(this);
		wave.gunLocation = new Point2D.Double(getX(), getY());
		GFTWave.targetLocation = GFTUtils.project(wave.gunLocation,
				enemyAbsoluteBearing, enemyDistance);
		wave.lateralDirection = lateralDirection;

		// Change bullet power here
		wave.bulletPower = chooseBulletPower();

		wave.setSegmentations(enemyDistance, enemyVelocity, lastEnemyVelocity);
		lastEnemyVelocity = enemyVelocity;
		wave.bearing = enemyAbsoluteBearing;
		setTurnGunRightRadians(Utils.normalRelativeAngle(enemyAbsoluteBearing
				- getGunHeadingRadians() + wave.mostVisitedBearingOffset()));

		if (getGunHeat() == 0) {
			// Notify team mates of this robot shooting
			FriendlyBulletMessage message = new FriendlyBulletMessage(
					new Point2D.Double(getX(), getY()), getTime(),
					bulletVelocity(wave.bulletPower), enemyAbsoluteBearing);
			try {
				broadcastMessage(message);
			} catch (IOException e1) {
				e1.printStackTrace();
			}
		}

		setFire(wave.bulletPower);
		if (getEnergy() >= wave.bulletPower) {
			addCustomEvent(wave);
		}
	}

	/*
	 * Added by Michael. Determines a fire power according to each robots
	 * position in a 4x4 playing grid
	 */
	private double chooseBulletPower() {
		int[] targetGridRef = new int[2];
		int[] myGridRef = new int[2];

		double xSplit = getBattleFieldWidth() / 4;
		double ySplit = getBattleFieldHeight() / 4;
		double targetX = GFTWave.targetLocation.getX();
		double targetY = GFTWave.targetLocation.getY();

		// Set x and y ref for target
		targetGridRef[0] = gridRef(targetX, xSplit);
		targetGridRef[1] = gridRef(targetY, ySplit);

		// Set x and y ref for self
		myGridRef[0] = gridRef(_myLocation.x, xSplit);
		myGridRef[1] = gridRef(_myLocation.y, ySplit);

		int zoneDist = Math.max(Math.abs(targetGridRef[0] - myGridRef[0]),
				Math.abs(targetGridRef[1] - myGridRef[1]));
		if (Math.abs(targetGridRef[0] - myGridRef[0]) == 2
				&& Math.abs(targetGridRef[1] - myGridRef[1]) == 2) {
			zoneDist++;
		} else if (Math.abs(targetGridRef[0] - myGridRef[0]) == 3
				&& Math.abs(targetGridRef[1] - myGridRef[1]) == 3) {
			return Rules.MIN_BULLET_POWER;
		}

		double bulletPower = Rules.MAX_BULLET_POWER - zoneDist + 1;
		if (bulletPower > Rules.MAX_BULLET_POWER) {
			return Rules.MAX_BULLET_POWER;
		}

		return bulletPower;
	}

	private int gridRef(double target, double split) {
		if (target < split) {
			return 0;
		} else if (target < (split * 2)) {
			return 1;
		} else if (target < (split * 3)) {
			return 2;
		} else {
			return 3;
		}
	}

	public void onRobotDeath(RobotDeathEvent e) {
		if (isTeammate(e.getName())) {
			// Change back to single robot strategy
			isTeamMates = false;
			_partnerTarget = null;
		}
	}

	public void updateWaves() {
		for (int x = 0; x < _enemyWaves.size(); x++) {
			EnemyWave ew = _enemyWaves.get(x);

			ew.distanceTraveled = (getTime() - ew.fireTime) * ew.bulletVelocity;
			if (ew.distanceTraveled > _myLocation.distance(ew.fireLocation) + 50) {
				_enemyWaves.remove(x);
				x--;
			}
		}
	}

	public EnemyWave getClosestSurfableWave() {
		double closestDistance = 50000;
		EnemyWave surfWave = null;

		for (int x = 0; x < _enemyWaves.size(); x++) {
			EnemyWave ew = _enemyWaves.get(x);
			double distance = _myLocation.distance(ew.fireLocation)
					- ew.distanceTraveled;

			if (distance > ew.bulletVelocity && distance < closestDistance) {
				surfWave = ew;
				closestDistance = distance;
			}
		}

		return surfWave;
	}

	// Given the EnemyWave that the bullet was on, and the point where we
	// were hit, calculate the index into our stat array for that factor.
	public static int getFactorIndex(EnemyWave ew, Point2D.Double targetLocation) {
		double offsetAngle = (absoluteBearing(ew.fireLocation, targetLocation) - ew.directAngle);
		double factor = Utils.normalRelativeAngle(offsetAngle)
				/ maxEscapeAngle(ew.bulletVelocity) * ew.direction;

		return (int) limit(0, (factor * ((BINS - 1) / 2)) + ((BINS - 1) / 2),
				BINS - 1);
	}

	// Given the EnemyWave that the bullet was on, and the point where we
	// were hit, update our stat array to reflect the danger in that area.
	public void logHit(EnemyWave ew, Point2D.Double targetLocation) {
		int index = getFactorIndex(ew, targetLocation);

		for (int x = 0; x < BINS; x++) {
			// for the spot bin that we were hit on, add 1;
			// for the bins next to it, add 1 / 2;
			// the next one, add 1 / 5; and so on...
			_surfStats[x] += 1.0 / (Math.pow(index - x, 2) + 1);
		}
	}

	public void onHitByBullet(HitByBulletEvent e) {
		if (!_enemyWaves.isEmpty()) {
			Point2D.Double hitBulletLocation = new Point2D.Double(e.getBullet()
					.getX(), e.getBullet().getY());
			EnemyWave hitWave = null;

			// look through the EnemyWaves, and find one that could've hit us.
			for (int x = 0; x < _enemyWaves.size(); x++) {
				EnemyWave ew = _enemyWaves.get(x);

				if (Math.abs(ew.distanceTraveled
						- _myLocation.distance(ew.fireLocation)) < 50
						&& Math.abs(bulletVelocity(e.getBullet().getPower())
								- ew.bulletVelocity) < 0.001) {
					hitWave = ew;
					break;
				}
			}

			if (hitWave != null) {
				logHit(hitWave, hitBulletLocation);

				// We can remove this wave now, of course.
				_enemyWaves.remove(_enemyWaves.lastIndexOf(hitWave));
			}
		}
	}

	// CREDIT: mini sized predictor from Apollon, by rozu
	// http://robowiki.net?Apollon
	public Point2D.Double predictPosition(EnemyWave surfWave, int direction) {
		Point2D.Double predictedPosition = (Point2D.Double) _myLocation.clone();
		double predictedVelocity = getVelocity();
		double predictedHeading = getHeadingRadians();
		double maxTurning, moveAngle, moveDir;

		int counter = 0; // number of ticks in the future
		boolean intercepted = false;

		do {
			moveAngle = wallSmoothing(predictedPosition,
					absoluteBearing(surfWave.fireLocation, predictedPosition)
							+ (direction * (Math.PI / 2)), direction)
					- predictedHeading;
			moveDir = 1;

			if (Math.cos(moveAngle) < 0) {
				moveAngle += Math.PI;
				moveDir = -1;
			}

			moveAngle = Utils.normalRelativeAngle(moveAngle);

			// maxTurning is built in like this, you can't turn more then this
			// in one tick
			maxTurning = Math.PI / 720d
					* (40d - 3d * Math.abs(predictedVelocity));
			predictedHeading = Utils.normalRelativeAngle(predictedHeading
					+ limit(-maxTurning, moveAngle, maxTurning));

			// if predictedVelocity and moveDir have different signs you want to
			// slow down otherwise you want to accelerate
			predictedVelocity += (predictedVelocity * moveDir < 0 ? 2 * moveDir
					: moveDir);
			predictedVelocity = limit(-8, predictedVelocity, 8);

			// calculate the new predicted position
			predictedPosition = project(predictedPosition, predictedHeading,
					predictedVelocity);

			counter++;

			if (predictedPosition.distance(surfWave.fireLocation) < surfWave.distanceTraveled
					+ (counter * surfWave.bulletVelocity)
					+ surfWave.bulletVelocity) {
				intercepted = true;
			}
		} while (!intercepted && counter < 500);

		return predictedPosition;
	}

	public double checkDanger(EnemyWave surfWave, int direction) {
		int index = getFactorIndex(surfWave,
				predictPosition(surfWave, direction));

		return _surfStats[index];
	}

	/*
	 * Added by Michael. Move away when hit
	 */
	public void onHitRobot(HitRobotEvent event) {
		if (event.getBearing() > -90 && event.getBearing() <= 90) {
			setBack(100);
		} else {
			setAhead(100);
		}
	}

	/*
	 * Added by Michael. Paints the enemy position and the bullet waves
	 */
	public void onPaint(Graphics2D g) {
		g.setColor(new Color(0, 255, 0));
		g.drawRect((int) _enemyLocation.x - 20, (int) _enemyLocation.y - 20,
				40, 40);

		for (int x = 0; x < _enemyWaves.size(); x++) {
			g.setColor(new Color(0, 255, 0));
			EnemyWave ew = _enemyWaves.get(x);
			double distance = (getTime() - ew.fireTime) * ew.bulletVelocity;

			g.drawOval((int) (ew.fireLocation.x - distance),
					(int) (ew.fireLocation.y - distance), (int) (distance * 2),
					(int) (distance * 2));

			Point2D.Double bullet = project(ew.fireLocation, ew.directAngle,
					distance);
			g.setColor(new Color(255, 255, 0));
			g.fillOval((int) bullet.x - 3, (int) bullet.y - 3, 6, 6);
		}

		g.setColor(new Color(0, 0, 255));
		g.drawRect((int) _partnerTarget.x - 20, (int) _partnerTarget.y - 20,
				40, 40);

		g.setColor(new Color(255, 0, 0));

		int x = (int) getBattleFieldWidth() / 4;
		for (int i = 1; i < 4; i++) {
			g.drawLine(x * i, 0, x * i, (int) getBattleFieldHeight());
		}

		int y = (int) getBattleFieldHeight() / 4;
		for (int i = 1; i < 4; i++) {
			g.drawLine(0, y * i, (int) getBattleFieldWidth(), y * i);
		}

	}

	public void doSurfing() {
		EnemyWave surfWave = getClosestSurfableWave();

		if (surfWave == null) {
			return;
		}

		double dangerLeft = checkDanger(surfWave, -1);
		double dangerRight = checkDanger(surfWave, 1);

		// Added by Michael. Calc distance to team mates. Prefer moving to side
		// with greater distance to team mates if not dangerous from bullets
		double teamToLeft = 0;
		double teamToRight = 0;
		for (Point2D.Double position : teamPositions.values()) {
			teamToLeft += predictPosition(surfWave, -1).distance(position);
			teamToRight += predictPosition(surfWave, 1).distance(position);
		}

		dangerLeft += (1 / Math.pow(teamToLeft, 2));
		dangerRight += (1 / Math.pow(teamToRight, 2));

		double goAngle = absoluteBearing(surfWave.fireLocation, _myLocation);
		if (dangerLeft < dangerRight) {
			goAngle = wallSmoothing(_myLocation, goAngle - (Math.PI / 2), -1);
		} else {
			goAngle = wallSmoothing(_myLocation, goAngle + (Math.PI / 2), 1);
		}

		setBackAsFront(this, goAngle);
	}

	// This can be defined as an inner class if you want.
	class EnemyWave {
		Point2D.Double fireLocation;
		long fireTime;
		double bulletVelocity, directAngle, distanceTraveled;
		int direction;

		public EnemyWave() {
		}
	}

	// CREDIT: Iterative WallSmoothing by Kawigi
	// - return absolute angle to move at after account for WallSmoothing
	// robowiki.net?WallSmoothing
	public double wallSmoothing(Point2D.Double botLocation, double angle,
			int orientation) {
		while (!_fieldRect.contains(project(botLocation, angle, 160))) {
			angle += orientation * 0.05;
		}
		return angle;
	}

	// CREDIT: from CassiusClay, by PEZ
	// - returns point length away from sourceLocation, at angle
	// robowiki.net?CassiusClay
	public static Point2D.Double project(Point2D.Double sourceLocation,
			double angle, double length) {
		return new Point2D.Double(sourceLocation.x + Math.sin(angle) * length,
				sourceLocation.y + Math.cos(angle) * length);
	}

	// got this from RaikoMicro, by Jamougha, but I think it's used by many
	// authors
	// - returns the absolute angle (in radians) from source to target points
	public static double absoluteBearing(Point2D.Double source,
			Point2D.Double target) {
		return Math.atan2(target.x - source.x, target.y - source.y);
	}

	public static double limit(double min, double value, double max) {
		return Math.max(min, Math.min(value, max));
	}

	public static double bulletVelocity(double power) {
		return (20D - (3D * power));
	}

	public static double maxEscapeAngle(double velocity) {
		return Math.asin(8.0 / velocity);
	}

	public static void setBackAsFront(AdvancedRobot robot, double goAngle) {
		double angle = Utils.normalRelativeAngle(goAngle
				- robot.getHeadingRadians());
		if (Math.abs(angle) > (Math.PI / 2)) {
			if (angle < 0) {
				robot.setTurnRightRadians(Math.PI + angle);
			} else {
				robot.setTurnLeftRadians(Math.PI - angle);
			}
			robot.setBack(100);
		} else {
			if (angle < 0) {
				robot.setTurnLeftRadians(-1 * angle);
			} else {
				robot.setTurnRightRadians(angle);
			}
			robot.setAhead(100);
		}
	}
}

class GFTWave extends Condition {
	static Point2D targetLocation;

	double bulletPower;
	Point2D gunLocation;
	double bearing;
	double lateralDirection;

	private static final double MAX_DISTANCE = 900;
	private static final int DISTANCE_INDEXES = 5;
	private static final int VELOCITY_INDEXES = 5;
	private static final int BINS = 25;
	private static final int MIDDLE_BIN = (BINS - 1) / 2;
	private static final double MAX_ESCAPE_ANGLE = 0.7;
	private static final double BIN_WIDTH = MAX_ESCAPE_ANGLE
			/ (double) MIDDLE_BIN;

	private static int[][][][] statBuffers = new int[DISTANCE_INDEXES][VELOCITY_INDEXES][VELOCITY_INDEXES][BINS];

	private int[] buffer;
	private AdvancedRobot robot;
	private double distanceTraveled;

	GFTWave(AdvancedRobot _robot) {
		this.robot = _robot;
	}

	public boolean test() {
		advance();
		if (hasArrived()) {
			buffer[currentBin()]++;
			robot.removeCustomEvent(this);
		}
		return false;
	}

	double mostVisitedBearingOffset() {
		return (lateralDirection * BIN_WIDTH) * (mostVisitedBin() - MIDDLE_BIN);
	}

	void setSegmentations(double distance, double velocity, double lastVelocity) {
		int distanceIndex = Math.min(DISTANCE_INDEXES - 1,
				(int) (distance / (MAX_DISTANCE / DISTANCE_INDEXES)));
		int velocityIndex = (int) Math.abs(velocity / 2);
		int lastVelocityIndex = (int) Math.abs(lastVelocity / 2);
		buffer = statBuffers[distanceIndex][velocityIndex][lastVelocityIndex];
	}

	private void advance() {
		distanceTraveled += GFTUtils.bulletVelocity(bulletPower);
	}

	private boolean hasArrived() {
		return distanceTraveled > gunLocation.distance(targetLocation) - 18;
	}

	private int currentBin() {
		int bin = (int) Math
				.round(((Utils.normalRelativeAngle(GFTUtils.absoluteBearing(
						gunLocation, targetLocation) - bearing)) / (lateralDirection * BIN_WIDTH))
						+ MIDDLE_BIN);
		return GFTUtils.minMax(bin, 0, BINS - 1);
	}

	private int mostVisitedBin() {
		int mostVisited = MIDDLE_BIN;
		for (int i = 0; i < BINS; i++) {
			if (buffer[i] > buffer[mostVisited]) {
				mostVisited = i;
			}
		}
		return mostVisited;
	}
}

class GFTUtils {
	static double bulletVelocity(double power) {
		return 20 - 3 * power;
	}

	static Point2D project(Point2D sourceLocation, double angle, double length) {
		return new Point2D.Double(sourceLocation.getX() + Math.sin(angle)
				* length, sourceLocation.getY() + Math.cos(angle) * length);
	}

	static double absoluteBearing(Point2D source, Point2D target) {
		return Math.atan2(target.getX() - source.getX(),
				target.getY() - source.getY());
	}

	static int sign(double v) {
		return v < 0 ? -1 : 1;
	}

	static int minMax(int v, int min, int max) {
		return Math.max(min, Math.min(max, v));
	}
}